const _ = require("../../utils/under-dash");

const RelType = require("../../xlsx/rel-type");

const colCache = require("../../utils/col-cache");
const Encryptor = require("../../utils/encryptor");
const Range = require("../../doc/range");
const StringBuf = require("../../utils/string-buf");
const Row = require("../../doc/row");
const Column = require("../../doc/column");

const SheetRelsWriter = require("./sheet-rels-writer");
const SheetCommentsWriter = require("./sheet-comments-writer");
const DataValidations = require("../../doc/data-validations");

const xmlBuffer = new StringBuf();

// ============================================================================================
// Xforms
const ListXform = require("../../xlsx/xform/list-xform");
const DataValidationsXform = require("../../xlsx/xform/sheet/data-validations-xform");
const SheetPropertiesXform = require("../../xlsx/xform/sheet/sheet-properties-xform");
const SheetFormatPropertiesXform = require("../../xlsx/xform/sheet/sheet-format-properties-xform");
const ColXform = require("../../xlsx/xform/sheet/col-xform");
const RowXform = require("../../xlsx/xform/sheet/row-xform");
const HyperlinkXform = require("../../xlsx/xform/sheet/hyperlink-xform");
const SheetViewXform = require("../../xlsx/xform/sheet/sheet-view-xform");
const SheetProtectionXform = require("../../xlsx/xform/sheet/sheet-protection-xform");
const PageMarginsXform = require("../../xlsx/xform/sheet/page-margins-xform");
const PageSetupXform = require("../../xlsx/xform/sheet/page-setup-xform");
const AutoFilterXform = require("../../xlsx/xform/sheet/auto-filter-xform");
const PictureXform = require("../../xlsx/xform/sheet/picture-xform");
const ConditionalFormattingsXform = require("../../xlsx/xform/sheet/cf/conditional-formattings-xform");
const HeaderFooterXform = require("../../xlsx/xform/sheet/header-footer-xform");
const RowBreaksXform = require("../../xlsx/xform/sheet/row-breaks-xform");
const SortedDimensions = require("../../utils/sorted-dimensions");
const Enums = require("../../doc/enums");
const RangeManager = require("../../doc/range-manager");
const Image = require("../../doc/image");
const utils = require("../../utils/utils");
const DrawingXform = require("../../xlsx/xform/sheet/drawing-xform");

const accessDeniedObject = new Proxy(
  {},
  {
    get() {
      throw new Error("工作表已提交，无法访问");
    },
    set() {
      throw new Error("工作表已提交，无法访问");
    },
    deleteProperty() {
      throw new Error("工作表已提交，无法访问");
    },
    defineProperty() {
      throw new Error("工作表已提交，无法访问");
    },
  }
);

// since prepare and render are functional, we can use singletons
const xform = {
  dataValidations: new DataValidationsXform(),
  sheetProperties: new SheetPropertiesXform(),
  sheetFormatProperties: new SheetFormatPropertiesXform(),
  columns: new ListXform({
    tag: "cols",
    length: false,
    childXform: new ColXform(),
  }),
  row: new RowXform(),
  hyperlinks: new ListXform({
    tag: "hyperlinks",
    length: false,
    childXform: new HyperlinkXform(),
  }),
  sheetViews: new ListXform({
    tag: "sheetViews",
    length: false,
    childXform: new SheetViewXform(),
  }),
  sheetProtection: new SheetProtectionXform(),
  pageMargins: new PageMarginsXform(),
  pageSeteup: new PageSetupXform(),
  autoFilter: new AutoFilterXform(),
  picture: new PictureXform(),
  conditionalFormattings: new ConditionalFormattingsXform(),
  headerFooter: new HeaderFooterXform(),
  rowBreaks: new RowBreaksXform(),
  drawing: new DrawingXform(),
};

// ============================================================================================

class WorksheetWriter {
  constructor(options) {
    // in a workbook, each sheet will have a number
    this.id = options.id;

    // and a name
    this.name = options.name || `Sheet${this.id}`;

    // add a state
    this.state = options.state || "visible";

    // rows are stored here while they need to be worked on.
    // when they are committed, they will be deleted.
    this._rows = [];

    // column definitions
    this._columns = null;

    // column keys (addRow convenience): key ==> this._columns index
    this._keys = {};

    // keep a record of all row and column pageBreaks
    this._merges = [];
    this._merges.add = function () {}; // ignore cell instruction

    // keep record of all hyperlinks
    this._sheetRelsWriter = new SheetRelsWriter(options);

    this._sheetCommentsWriter = new SheetCommentsWriter(
      this,
      this._sheetRelsWriter,
      options
    );

    // keep a record of dimensions
    this._dimensions = new Range();

    // first uncommitted row
    this._rowZero = 1;

    // committed flag
    this.committed = false;

    // for data validations
    this.dataValidations = new DataValidations();

    // for sharing formulae
    this._formulae = {};
    this._siFormulae = 0;

    // keep a record of conditionalFormattings
    this.conditionalFormatting = [];

    // keep a record of all row and column pageBreaks
    this.rowBreaks = [];

    // for default row height, outline levels, etc
    this.properties = Object.assign(
      {},
      {
        defaultRowHeight: 15,
        dyDescent: 55,
        outlineLevelCol: 0,
        outlineLevelRow: 0,
      },
      options.properties
    );

    this.headerFooter = Object.assign(
      {},
      {
        differentFirst: false,
        differentOddEven: false,
        oddHeader: null,
        oddFooter: null,
        evenHeader: null,
        evenFooter: null,
        firstHeader: null,
        firstFooter: null,
      },
      options.headerFooter
    );

    // for all things printing
    this.pageSetup = Object.assign(
      {},
      {
        margins: {
          left: 0.7,
          right: 0.7,
          top: 0.75,
          bottom: 0.75,
          header: 0.3,
          footer: 0.3,
        },
        orientation: "portrait",
        horizontalDpi: 4294967295,
        verticalDpi: 4294967295,
        fitToPage: !!(
          options.pageSetup &&
          (options.pageSetup.fitToWidth || options.pageSetup.fitToHeight) &&
          !options.pageSetup.scale
        ),
        pageOrder: "downThenOver",
        blackAndWhite: false,
        draft: false,
        cellComments: "None",
        errors: "displayed",
        scale: 100,
        fitToWidth: 1,
        fitToHeight: 1,
        paperSize: undefined,
        showRowColHeaders: false,
        showGridLines: false,
        horizontalCentered: false,
        verticalCentered: false,
        rowBreaks: null,
        colBreaks: null,
      },
      options.pageSetup
    );

    // using shared strings creates a smaller xlsx file but may use more memory
    this.useSharedStrings = options.useSharedStrings || false;

    this._workbook = options.workbook;

    this.hasComments = false;

    // views
    this._views = options.views || [];

    // auto filter
    this.autoFilter = options.autoFilter || null;

    this._media = [];

    // worksheet protection
    this.sheetProtection = null;

    // start writing to stream now
    this._writeOpenWorksheet();

    this.startedData = false;

    this.sortedDimensions = new SortedDimensions();
  }

  get workbook() {
    return this._workbook;
  }

  get stream() {
    if (!this._stream) {
      // eslint-disable-next-line no-underscore-dangle
      this._stream = this._workbook._openStream(
        `/xl/worksheets/sheet${this.id}.xml`
      );

      // pause stream to prevent 'data' events
      this._stream.pause();
    }
    return this._stream;
  }

  // destroy - not a valid operation for a streaming writer
  // even though some streamers might be able to, it's a bad idea.
  destroy() {
    throw new Error("Invalid Operation: destroy");
  }

  commitRows() {
    if (this.committed) {
      return;
    }
    // commit all rows
    const lastRow = this.lastRow;
    if (lastRow) {
      this._commitRow(lastRow);
      this._rows = [];
    }
  }

  commitRowsAsync() {
    return new Promise((resolve) => {
      setImmediate(() => {
        this.commitRows();
        resolve();
      });
    });
  }

  commit() {
    if (this.committed) {
      return;
    }
    // commit all rows
    this.commitRows();

    // we _cannot_ accept new rows from now on
    this._rows = accessDeniedObject;

    if (!this.startedData) {
      this._writeOpenSheetData();
    }
    this._writeCloseSheetData();
    this._writeAutoFilter();
    this._writeMergeCells();

    // for some reason, Excel can't handle dimensions at the bottom of the file
    // this._writeDimensions();

    // https://github.com/exceljs/exceljs/pull/2803
    // 根据issue，conditional formatting要在hyperlinks之前
    this._writeConditionalFormatting();
    this._writeHyperlinks();
    this._writeDataValidations();
    this._writeSheetProtection();
    this._writePageMargins();
    this._writePageSetup();
    this._writeBackground();
    this._writeHeaderFooter();
    this._writeRowBreaks();
    this._writeDrawings();

    // Legacy Data tag for comments
    this._writeLegacyData();

    this._writeCloseWorksheet();
    // signal end of stream to workbook
    this.stream.end();

    this._sheetCommentsWriter.commit();
    // also commit the hyperlinks if any
    this._sheetRelsWriter.commit();

    this.committed = true;
  }

  commitAsync() {
    return new Promise((resolve) => {
      setImmediate(() => {
        this.commit();
        resolve();
      });
    });
  }

  // return the current dimensions of the writer
  get dimensions() {
    return this._dimensions;
  }

  get views() {
    return this._views;
  }

  // =========================================================================
  // Columns

  // get the current columns array.
  get columns() {
    return this._columns;
  }

  set columns(value) {
    this.setColumns(value, 1);
  }

  // set the columns from an array of column definitions.
  // Note: any headers defined will overwrite existing values.
  setColumns(value, rowNumber) {
    // calculate max header row count
    // 没用到的字段，不知道为什么要赋值
    // this._headerRowCount = value.reduce((pv, cv) => {
    //   const headerCount = (cv.header && 1) || (cv.headers && cv.headers.length) || 0;
    //   return Math.max(pv, headerCount);
    // }, 0);

    // construct Column objects
    let count = 1;
    const columns = (this._columns = []);
    value.forEach((defn) => {
      // 确保设置rowNumber以后再设置defn
      const column = new Column(this, count++, false);
      // 先push，确保设置header时创建的cell引用的column时这里构造的column
      columns.push(column);

      if (rowNumber != null) {
        column.rowNumber = rowNumber;
      }

      column.defn = defn;
    });
  }

  getColumnKey(key) {
    return this._keys[key];
  }

  setColumnKey(key, value) {
    this._keys[key] = value;
  }

  deleteColumnKey(key) {
    delete this._keys[key];
  }

  eachColumnKey(f) {
    _.each(this._keys, f);
  }

  // get a single column by col number. If it doesn't exist, it and any gaps before it
  // are created.
  getColumn(c) {
    if (typeof c === "string") {
      // if it matches a key'd column, return that
      const col = this._keys[c];
      if (col) return col;

      // otherwise, assume letter
      c = colCache.l2n(c);
    }
    if (!this._columns) {
      this._columns = [];
    }
    if (c > this._columns.length) {
      let n = this._columns.length + 1;
      while (n <= c) {
        this._columns.push(new Column(this, n++));
      }
    }
    return this._columns[c - 1];
  }

  // =========================================================================
  // Rows
  get _nextRow() {
    return this._rowZero + this._rows.length;
  }

  // iterate over every uncommitted row in the worksheet, including maybe empty rows
  eachRow(options, iteratee) {
    if (!iteratee) {
      iteratee = options;
      options = undefined;
    }
    if (options && options.includeEmpty) {
      const n = this._nextRow;
      for (let i = this._rowZero; i < n; i++) {
        iteratee(this.getRow(i), i);
      }
    } else {
      this._rows.forEach((row) => {
        if (row.hasValues) {
          iteratee(row, row.number);
        }
      });
    }
  }

  _commitRow(cRow) {
    // since rows must be written in order, we commit all rows up till and including cRow
    let found = false,
      index = 0;
    while (this._rows.length && !found) {
      const row = this._rows[index++];
      this._rowZero++;
      if (row) {
        this._writeRow(row);
        found = row.number === cRow.number;
        this._rowZero = row.number + 1;
      }
    }
    this._rows = this._rows.slice(index);
  }

  // 检查要访问的行数是否已提交，若已提交则抛出异常；否则返回相对this._rows的下标
  _checkRowNo(rowNo) {
    const index = rowNo - this._rowZero;
    if (index < 0) {
      throw new Error(`第${this._rowZero}之前行已经提交，无法访问第${rowNo}行`);
    }
    return index;
  }

  get lastRow() {
    // returns last uncommitted row
    if (this._rows.length) {
      return this._rows[this._rows.length - 1];
    }
    return undefined;
  }

  // find a row (if exists) by row number
  findRow(rowNumber) {
    const index = this._checkRowNo(rowNumber);
    return this._rows[index];
  }

  findRows(start, length) {
    let count = length;
    const rows = [];
    while (count--) {
      rows.push(this.findRow(start + length - count - 1));
    }
    return rows;
  }

  getRow(rowNumber) {
    const index = this._checkRowNo(rowNumber);
    let row = this._rows[index];
    if (!row) {
      this._rows[index] = row = new Row(this, rowNumber);
    }
    return row;
  }

  getRows(start, length) {
    let count = length;
    const rows = [];
    while (count--) {
      rows.push(this.getRow(start + length - count - 1));
    }
    return rows;
  }

  select(...address) {
    const dimensions = new Range(address);
    return new RangeManager(this, dimensions);
  }

  selectRow(rowNo) {
    const row = this.getRow(rowNo);
    return this.select(rowNo, 0, rowNo, row.cellCount);
  }

  selectColumn(colNo) {
    return this.select(0, colNo, this._nextRow - 1, colNo);
  }

  addRow(value) {
    const row = new Row(this, this._nextRow);
    this._rows[row.number - this._rowZero] = row;
    row.values = value;
    return row;
  }

  addRows(values) {
    return values.map((v) => this.addRow(v));
  }

  setRow(rowNo, value) {
    const row = this.getRow(rowNo);
    row.values = value;
    return row;
  }

  setRows(start, values) {
    let offset = 0;
    return values.map((value) => {
      return this.setRow(start + offset++, value);
    });
  }

  insertRow(pos, value) {
    this.spliceRows(pos, 0, value);
    return this.getRow(pos);
  }

  insertRows(pos, values) {
    this.spliceRows(pos, 0, ...values);
    return this.getRows(pos, values.length);
  }

  duplicateRow(rowNum, count, insert = false) {
    // create count duplicates of rowNum
    // either inserting new or overwriting existing rows
    const index = this._checkRowNo(rowNum);

    const rSrc = this._rows[index];
    const inserts = new Array(count).fill(rSrc.values);
    this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);

    // now copy styles...
    for (let i = 0; i < count; i++) {
      const rDst = this._rows[index + i];
      rDst.style = rSrc.style;
      rDst.height = rSrc.height;
      // eslint-disable-next-line no-loop-func
      rSrc.eachCell({ includeEmpty: true }, (cell, colNumber) => {
        if (cell._style) {
          // 自定义样式时才复制，否则沿用行列的
          rDst.getCell(colNumber).style = cell.style;
        }
      });
    }
  }

  spliceRows(start, count, ...inserts) {
    // same problem as row.splice, except worse.

    this._checkRowNo(start);

    // 无法在合并的单元格中间插入行
    const mergedDimensions = this.sortedDimensions;
    const merge = mergedDimensions.findIntersection(
      new Range(start, mergedDimensions.left, start, mergedDimensions.right)
    );
    if (merge && merge.top < start) {
      throw new Error(
        `无法在第${start}行插入数据，位于合并的单元格区间内：top: ${merge.top} left: ${merge.left} bottom: ${merge.bottom} right: ${merge.right}`
      );
    }

    const nKeep = start + count;
    const nInserts = inserts.length;
    const nExpand = nInserts - count;
    const nEnd = this._rows.length;
    let i;
    let rSrc;
    if (nExpand < 0) {
      // remove rows
      for (i = nKeep; i <= nEnd; i++) {
        rSrc = this._rows[i - 1];
        if (rSrc) {
          const rDst = this.getRow(i + nExpand);
          rDst.values = rSrc.values;
          rDst.style = rSrc.style;
          rDst.height = rSrc.height;
          // eslint-disable-next-line no-loop-func
          rSrc.eachCell({ includeEmpty: true }, (cell, colNumber) => {
            if (cell._style) {
              rDst.getCell(colNumber).style = cell.style;
            }
          });
          this._rows[i - 1] = undefined;
        } else {
          this._rows[i + nExpand - 1] = undefined;
        }
      }
    } else if (nExpand > 0) {
      // insert new cells
      for (i = nEnd; i >= nKeep; i--) {
        rSrc = this._rows[i - 1];
        if (rSrc) {
          const rDst = this.getRow(i + nExpand);
          rDst.values = rSrc.values;
          rDst.style = rSrc.style;
          rDst.height = rSrc.height;
          // eslint-disable-next-line no-loop-func
          rSrc.eachCell({ includeEmpty: true }, (cell, colNumber) => {
            if (cell._style) {
              rDst.getCell(colNumber).style = cell.style;
            }
          });
        } else {
          this._rows[i + nExpand - 1] = undefined;
        }
      }
    }

    // now copy over the new values
    for (i = 0; i < nInserts; i++) {
      const rDst = this.getRow(start + i);
      rDst.style = {};
      rDst.values = inserts[i];
    }

    // account for defined names
    this.workbook.definedNames.spliceRows(this.name, start, count, nInserts);
  }

  // ================================================================================
  // Cells

  // returns the cell at [r,c] or address given by r. If not found, return undefined
  findCell(r, c) {
    const address = colCache.getAddress(r, c);
    const row = this.findRow(address.row);
    return row ? row.findCell(address.col) : undefined;
  }

  // return the cell at [r,c] or address given by r. If not found, create a new one.
  getCell(r, c) {
    const address = colCache.getAddress(r, c);
    const row = this.getRow(address.row);
    return row.getCellEx(address);
  }

  mergeCells(...cells) {
    // may fail if rows have been comitted
    const dimensions = new Range(cells);
    this._checkRowNo(dimensions.top);

    // check cells aren't already merged
    const intersection =
      this.sortedDimensions.findIntersectionOrAdd(dimensions);
    if (intersection !== false) {
      throw new Error(`无法合并已经被合并的单元格，要合并的单元格：top: ${dimensions.top} left: ${dimensions.left} bottom: ${dimensions.bottom} right: ${dimensions.right}
                                   已合并的单元格：top: ${intersection.top} left: ${intersection.left} bottom: ${intersection.bottom} right: ${intersection.right}`);
    }

    // apply merge
    const master = this.getCell(dimensions.top, dimensions.left);
    for (let i = dimensions.top; i <= dimensions.bottom; i++) {
      for (let j = dimensions.left; j <= dimensions.right; j++) {
        if (i > dimensions.top || j > dimensions.left) {
          this.getCell(i, j).merge(master);
        }
      }
    }

    // index merge
    this._merges.push(dimensions);
  }

  _unMergeMaster(master) {
    // master is always top left of a rectangle
    const merge = this.sortedDimensions.remove(new Range(master.address));
    if (merge) {
      for (let i = merge.top; i <= merge.bottom; i++) {
        for (let j = merge.left; j <= merge.right; j++) {
          const cell = this.findCell(i, j);
          if (cell) {
            cell.unmerge();
          }
        }
      }
      const index = this._merges.findIndex((m) => m === merge);
      if (index >= 0) {
        this._merges.splice(index, 1);
      }
    }
  }

  unMergeCells(...cells) {
    const dimensions = new Range(cells);
    this._checkRowNo(dimensions.top);

    // find any cells in that range and unmerge them
    for (let i = dimensions.top; i <= dimensions.bottom; i++) {
      for (let j = dimensions.left; j <= dimensions.right; j++) {
        const cell = this.findCell(i, j);
        if (cell) {
          if (cell.type === Enums.ValueType.Merge) {
            // this cell merges to another master
            this._unMergeMaster(cell.master);
          } else {
            // this cell is a master
            this._unMergeMaster(cell);
          }
        }
      }
    }
  }

  // ===========================================================================
  // Conditional Formatting
  addConditionalFormatting(cf) {
    this.conditionalFormatting.push(cf);
  }

  removeConditionalFormatting(filter) {
    if (typeof filter === "number") {
      this.conditionalFormatting.splice(filter, 1);
    } else if (filter instanceof Function) {
      this.conditionalFormatting = this.conditionalFormatting.filter(filter);
    } else {
      this.conditionalFormatting = [];
    }
  }

  // =========================================================================

  addBackgroundImage(imageId) {
    this._background = {
      imageId,
    };
  }

  getBackgroundImageId() {
    return this._background && this._background.imageId;
  }

  // =========================================================================
  // Worksheet Protection
  protect(password, options) {
    // TODO: make this function truly async
    // perhaps marshal to worker thread or something
    return new Promise((resolve) => {
      this.sheetProtection = {
        sheet: true,
      };
      if (options && "spinCount" in options) {
        // force spinCount to be integer >= 0
        options.spinCount = Number.isFinite(options.spinCount)
          ? Math.round(Math.max(0, options.spinCount))
          : 100000;
      }
      if (password) {
        this.sheetProtection.algorithmName = "SHA-512";
        this.sheetProtection.saltValue =
          Encryptor.randomBytes(16).toString("base64");
        this.sheetProtection.spinCount =
          options && "spinCount" in options ? options.spinCount : 100000; // allow user specified spinCount
        this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(
          password,
          "SHA512",
          this.sheetProtection.saltValue,
          this.sheetProtection.spinCount
        );
      }
      if (options) {
        this.sheetProtection = Object.assign(this.sheetProtection, options);
        if (!password && "spinCount" in options) {
          delete this.sheetProtection.spinCount;
        }
      }
      resolve();
    });
  }

  unprotect() {
    this.sheetProtection = null;
  }

  // ================================================================================

  _write(text) {
    xmlBuffer.reset();
    xmlBuffer.addText(text);
    this.stream.write(xmlBuffer);
  }

  _writeSheetProperties(xmlBuf, properties, pageSetup) {
    const sheetPropertiesModel = {
      outlineProperties: properties && properties.outlineProperties,
      tabColor: properties && properties.tabColor,
      pageSetup:
        pageSetup && pageSetup.fitToPage
          ? {
              fitToPage: pageSetup.fitToPage,
            }
          : undefined,
    };

    xmlBuf.addText(xform.sheetProperties.toXml(sheetPropertiesModel));
  }

  _writeSheetFormatProperties(xmlBuf, properties) {
    const sheetFormatPropertiesModel = properties
      ? {
          defaultRowHeight: properties.defaultRowHeight,
          dyDescent: properties.dyDescent,
          outlineLevelCol: properties.outlineLevelCol,
          outlineLevelRow: properties.outlineLevelRow,
        }
      : undefined;
    if (properties.defaultColWidth) {
      sheetFormatPropertiesModel.defaultColWidth = properties.defaultColWidth;
    }

    xmlBuf.addText(
      xform.sheetFormatProperties.toXml(sheetFormatPropertiesModel)
    );
  }

  _writeOpenWorksheet() {
    xmlBuffer.reset();

    xmlBuffer.addText(
      '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
    );
    xmlBuffer.addText(
      '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"' +
        ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"' +
        ' xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"' +
        ' mc:Ignorable="x14ac"' +
        ' xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">'
    );

    this._writeSheetProperties(xmlBuffer, this.properties, this.pageSetup);

    xmlBuffer.addText(xform.sheetViews.toXml(this.views));

    this._writeSheetFormatProperties(xmlBuffer, this.properties);

    this.stream.write(xmlBuffer);
  }

  _writeColumns() {
    const cols = Column.toModel(this.columns);
    if (cols) {
      xform.columns.prepare(cols, { styles: this._workbook.styles });
      this.stream.write(xform.columns.toXml(cols));
    }
  }

  _writeOpenSheetData() {
    this._write("<sheetData>");
  }

  _writeRow(row) {
    if (!this.startedData) {
      this._writeColumns();
      this._writeOpenSheetData();
      this.startedData = true;
    }

    if (row.hasValues || row.height) {
      const { model } = row;
      const options = {
        styles: this._workbook.styles,
        sharedStrings: this.useSharedStrings
          ? this._workbook.sharedStrings
          : undefined,
        hyperlinks: this._sheetRelsWriter.hyperlinksProxy,
        merges: this._merges,
        formulae: this._formulae,
        siFormulae: this._siFormulae,
        comments: [],
      };
      xform.row.prepare(model, options);
      this.stream.write(xform.row.toXml(model));

      if (options.comments.length) {
        this.hasComments = true;
        this._sheetCommentsWriter.addComments(options.comments);
      }
    }
  }

  _writeCloseSheetData() {
    this._write("</sheetData>");
  }

  _writeMergeCells() {
    if (this._merges.length) {
      xmlBuffer.reset();
      xmlBuffer.addText(`<mergeCells count="${this._merges.length}">`);
      this._merges.forEach((merge) => {
        xmlBuffer.addText(`<mergeCell ref="${merge}"/>`);
      });
      xmlBuffer.addText("</mergeCells>");

      this.stream.write(xmlBuffer);
    }
  }

  _writeHyperlinks() {
    // eslint-disable-next-line no-underscore-dangle
    this.stream.write(
      xform.hyperlinks.toXml(this._sheetRelsWriter._hyperlinks)
    );
  }

  _writeConditionalFormatting() {
    const options = {
      styles: this._workbook.styles,
    };
    xform.conditionalFormattings.prepare(this.conditionalFormatting, options);
    this.stream.write(
      xform.conditionalFormattings.toXml(this.conditionalFormatting)
    );
  }

  _writeRowBreaks() {
    this.stream.write(xform.rowBreaks.toXml(this.rowBreaks));
  }

  _writeDataValidations() {
    this.stream.write(xform.dataValidations.toXml(this.dataValidations.model));
  }

  _writeSheetProtection() {
    this.stream.write(xform.sheetProtection.toXml(this.sheetProtection));
  }

  _writePageMargins() {
    this.stream.write(xform.pageMargins.toXml(this.pageSetup.margins));
  }

  _writePageSetup() {
    this.stream.write(xform.pageSeteup.toXml(this.pageSetup));
  }

  _writeHeaderFooter() {
    this.stream.write(xform.headerFooter.toXml(this.headerFooter));
  }

  _writeAutoFilter() {
    this.stream.write(xform.autoFilter.toXml(this.autoFilter));
  }

  _writeBackground() {
    if (this._background) {
      if (this._background.imageId !== undefined) {
        const image = this._workbook.getImage(this._background.imageId);
        const pictureId = this._sheetRelsWriter.addMedia({
          Target: `../media/${image.name}`,
          Type: RelType.Image,
        });

        this._background = {
          ...this._background,
          rId: pictureId,
        };
      }
      this.stream.write(xform.picture.toXml({ rId: this._background.rId }));
    }
  }

  _writeLegacyData() {
    if (this.hasComments) {
      xmlBuffer.reset();
      xmlBuffer.addText(
        `<legacyDrawing r:id="${this._sheetCommentsWriter.vmlRelId}"/>`
      );
      this.stream.write(xmlBuffer);
    }
  }

  _writeDimensions() {
    // for some reason, Excel can't handle dimensions at the bottom of the file
    // and we don't know the dimensions until the commit, so don't write them.
    // this._write('<dimension ref="' + this._dimensions + '"/>');
  }

  _writeCloseWorksheet() {
    this._write("</worksheet>");
  }

  _prepareDrawings() {
    const nextRid = utils.ridGenerator();
    // drawing relations
    const relations = [];
    // drawing anchors
    const anchors = [];
    const workbook = this._workbook;
    let bookImage;
    this._media.forEach((medium) => {
      if (medium.type === "image") {
        bookImage = workbook.getImage(medium.imageId);
        const relRid = nextRid();
        relations.push({
          Id: relRid,
          Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
          // name已经包含extension
          Target: `../media/${bookImage.name}`,
        });

        const anchor = {
          picture: {
            rId: relRid,
          },
          range: medium.range,
        };
        if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
          const rIdHyperLink = nextRid();
          anchor.picture.hyperlinks = {
            tooltip: medium.hyperlinks.tooltip,
            rId: rIdHyperLink,
          };
          relations.push({
            Id: rIdHyperLink,
            Type: RelType.Hyperlink,
            Target: medium.hyperlinks.hyperlink,
            TargetMode: "External",
          });
        }
        anchors.push(anchor);
      } else {
        throw new Error("仅支持type = image");
      }
    });
    return {
      relations,
      anchors,
    };
  }

  _writeDrawings() {
    const { relations, anchors } = this._prepareDrawings();
    const workbook = this._workbook;
    // 让workbook渲染drawing和rels文件
    const drawing = {
      rid: this.id,
      // 每个sheet对应一个drawing，drawing命名直接采用this.id
      name: `drawing${this.id}`,
      anchors,
      rels: relations,
    };
    workbook.drawings.push(drawing);

    // 向worksheet rels添加关联
    const drawingId = this._sheetRelsWriter.addMedia({
      Target: `../drawings/${drawing.name}.xml`,
      Type: RelType.Drawing,
    });
    // 向worksheet添加drawing标签
    this.stream.write(xform.drawing.toXml({ rId: drawingId }));
  }

  addImage(imageId, range) {
    const model = {
      type: "image",
      imageId,
      range,
    };
    this._media.push(new Image(this, model));
  }
}

module.exports = WorksheetWriter;
